package hips;

import java.awt.Color;

import hips.tools.Neighborhood;
import hips.tools.RegionRoi;
import ij.ImagePlus;
import ij.gui.Overlay;

/**
 * Clase abstracta que almacena una instancia de la clase <i>ImagePlus</i> de ImageJ y
 * proporciona diversos métodos para el acceso y modificación de la imagen, sin
 * importar su tipo, apoyándose en el uso de la interface <i>PixelValue</i>.
 */
public abstract class Image<PixelValue_ extends PixelValue<PixelValue_>> {
	protected ImagePlus imagePlus;
	protected int slices;
	protected int channels;
	protected int size;
	protected int height;
	protected int width;
	protected PixelValue_ maxValue;
	protected PixelValue_ minValue;
	protected PixelValue_ maxRange;

	private RegionRoi roi;

	protected void initialize(ImagePlus impl) {
		maxValue = null;
		minValue = null;
		maxRange = null;
		imagePlus = impl;
		Overlay overlay = new Overlay();
		roi = new RegionRoi(impl);
		setColorOfInterest(Color.yellow);
		overlay.add(roi);
		impl.setOverlay(overlay);
	}

	/**
	 * Método estático que devuelve una instancia de esta clase a partir de una
	 * instancia de la clase ImagePlus, propia de ImageJ.
	 * 
	 * @return Dependiendo del tipo de imagen que se proporcione en <i>imp</i>,
	 *         devolverá una instancia de alguna de las clases de imagenes
	 *         soportadas: ImageRGB, ImageGray8, ImageGray16, ImageGray32, o
	 *         NULL si el tipo de imagen no está soportado en HIPS.
	 */
	public static Image<?> getImage(ImagePlus imp) {
		if (imp == null) {
			return null;
		}
		if (!imp.isProcessor()) {
			return null;
		}
		if (imp.getType() == ImagePlus.COLOR_RGB)
			return new hips.images.integer.rgb.ImageRGB(imp);
		else if (imp.getType() == ImagePlus.GRAY8)
			return new hips.images.integer.gray8.ImageGray8(imp);
		else if (imp.getType() == ImagePlus.GRAY16)
			return new hips.images.integer.gray16.ImageGray16(imp);
		else if (imp.getType() == ImagePlus.GRAY32)
			return new hips.images.floating.gray32.ImageGray32(imp);
		return null;
	}

	/**
	 * Devuelve una nueva instancia de la clase <i>PixelValue</i>.
	 * 
	 * @param value
	 *            Valor que será asignado a todos los canales del píxel.
	 */
	public abstract PixelValue_ newPixelValue(Number value);

	/**
	 * Devuelve una nueva instancia de la clase <i>PixelValue</i>. Se le asigna
	 * el valor cero a cada uno de los canales del píxel.
	 */
	public abstract PixelValue_ newPixelValue();

	/**
	 * Crea una nueva imagen de las mismas dimensiones, con el mismo número de
	 * canales y con el mismo tipo de píxel.
	 */
	public abstract Image<PixelValue_> newImage();

	/**
	 * Obtiene el valor del píxel situado en <i>index</i>.
	 * 
	 * @param index
	 *            Posición del píxel dentro de la imagen. Se calcula como: <i>x
	 *            * ancho + y</i>.
	 */
	public abstract PixelValue_ getPixelValue(int index);

	/**
	 * Obtiene el valor del píxel situado en <i>index</i>.
	 * 
	 * @param index
	 *            Posición del píxel dentro de la imagen. Se calcula como: <i>x
	 *            * ancho + y</i>.
	 * @param p
	 *            Instancia de PixelValue en la que se devolverá el valor del
	 *            píxel solicitado. De esta manera, no es necesario instanciar
	 *            ningún objeto en esta llamada.
	 */
	public abstract PixelValue_ getPixelValue(PixelValue_ p, int index);

	/**
	 * Escribe el valor <i>pvalue</i> en el píxel situado en <i>index</i>.
	 * 
	 * @param index
	 *            Posición del píxel dentro de la imagen. Se calcula como: <i>x
	 *            * ancho + y</i>.
	 * @param p
	 *            Valor a asignar.
	 */
	public abstract void putPixelValue(int index, PixelValue_ p);

	/**
	 * Devuelve una instancia de <i>PixelValue</i> con el valor medio de los
	 * píxeles que conforman la región <i>r</i>.
	 */
	public PixelValue_ getMeanValue(Region region) {
		PixelValue_ meanValue = newPixelValue();
		PixelValue_ p = newPixelValue();
		for (int i = 0; i < region.getSize(); i++) {
			meanValue.add(getPixelValue(p, region.getPixelPosition(i)));
		}
		meanValue.div(region.getSize());
		return meanValue;
	}

	/**
	 * Pinta los píxeles de la región <i>r</i> con el valor que se especifica en
	 * <i>p</i>.
	 * 
	 * @param r
	 *            Región que contiene los píxeles que van a ser pintados en la
	 *            imagen.
	 * @param p
	 *            Valor con el que va a ser pintada la región de la imagen.
	 */
	public void paintRegion(Region r, PixelValue_ p) {
		for (int i = 0; i < r.getSize(); i++) {
			putPixelValue(r.getPixelPosition(i), p);
		}
	}

	/**
	 * Selecciona una region de la imagen para que sea coloreada con un color
	 * que tiene que ser especificado anteriormente mediante la función
	 * <i>setRegionOfInterest</i>.
	 * 
	 */
	public void setRegionOfInterest(Region region) {
		Region old = roi.getRegion();
		roi.setRegion(region);
		if ((region == null && old == null) || (region != null && old != null && old.sameAs(region))) {
			return;
		}
		imagePlus.updateAndDraw();
	}

	/**
	 * Especifica el color que se usará para colorear las regiones de interés en
	 * esta imagen.
	 */
	public void setColorOfInterest(Color c) {
		roi.setFillColor(c);
	}

	/**
	 * Pinta la imagen completa con el valor cero en cada uno de los píxeles.
	 */
	public void clear() {
		PixelValue_ p = newPixelValue();
		for (int i = 0; i < size; i++) {
			putPixelValue(i, p);
		}
		imagePlus.updateAndDraw();
	}

	/**
	 * Devuelve el número total de píxeles de la imagen.
	 */
	public int getSize() {
		return size;
	}

	/**
	 * Devuelve el ancho de la imagen, en píxeles.
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * Devuelve el alto de la imagen, en píxeles.
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * Devuelve la instancia de la clase <i>ImagePlus</i> a partir de la cual se
	 * creó la imagen.
	 */
	public ImagePlus getImagePlus() {
		return imagePlus;
	}

	/**
	 * Obtiene el título de la imagen.
	 */
	public String getTitle() {
		return imagePlus.getTitle();
	}

	/**
	 * Establece el título de la imagen.
	 */
	public void setTitle(String title) {
		imagePlus.setTitle(title);
	}

	/**
	 * Devuelve el número de slices de la imagen.
	 */
	public int getSlices() {
		return slices;
	}

	/**
	 * Devuelve el número de channels de la imagen.
	 */
	public int getChannels() {
		return channels;
	}

	/**
	 * Devuelve una nueva instancia de la clase <i>Partition</i> mediante la
	 * cual la imagen podrá ser segmentada. La partición creada estará
	 * caracterizada por los parámetros que se especifican en la llamada.
	 * 
	 * @param alpha
	 *            Parámetro de rango local.
	 * @param omega
	 *            Parámetro de rango global.
	 * @param cindex
	 *            Índice de conectividad.
	 * @param type
	 *            Indica que clase de partición se va a instanciar. Puede valer
	 *            uno de estos 3 valores: Partition.ORIGINAL,
	 *            Partition.IMPROVED_WITHOUT_CINDEX,
	 *            Partition.IMPROVED_WITH_CINDEX
	 * @param randomPixels
	 *            Establece el número de píxeles que se examinarán de manera
	 *            aleatoria al comienzo de la llamada al método
	 *            <i>makeRegions</i> de la clase <i>Partition</i>
	 *            correspodiente.
	 */
	public abstract Partition<PixelValue_> newPartition(int type, PixelValue_ alpha, PixelValue_ omega, float ci, int randomPixels);

	/**
	 * Devuelve una nueva instancia de la clase <i>Partition</i> mediante la
	 * cual la imagen podrá ser segmentada. La partición que se cree estará
	 * caracterizada por unos parámetros predefinidos.
	 *@param type
	 *            Indica que clase de partición se va a instanciar. Puede valer
	 *            uno de estos 3 valores: Partition.ORIGINAL,
	 *            Partition.IMPROVED_WITHOUT_CINDEX,
	 *            Partition.IMPROVED_WITH_CINDEX
	 */
	public abstract Partition<PixelValue_> newPartition(int type);

	/**
	 * Obtiene la diferencia del valor de intensidad entre píxeles consecutivos
	 * más grande en toda la imagen. Este valor se calcula de manera independiente
	 * en cada <i>slice</i>.
	 */
	public PixelValue_ getMaxRange() {
		if (maxRange == null) {
			calculateMaxRange();
		}
		return maxRange.copy();
	}

	/**
	 * Obtiene el máximo valor de intensidad de un píxel que hay en la imagen.
	 */
	public PixelValue_ getMaxValue() {
		if (maxValue == null) {
			calculateBounds();
		}
		return maxValue.copy();
	}

	/**
	 * Obtiene el mínimo valor de intensidad de un píxel que hay en la imagen.
	 */
	public PixelValue_ getMinValue() {
		if (minValue == null) {
			calculateBounds();
		}
		return minValue.copy();
	}

	private void calculateBounds() {
		int n = width * height;
		maxValue = getPixelValue(0);
		minValue = getPixelValue(0);
		PixelValue_ p = newPixelValue();
		for (int i = 0; i < n; i++) {
			getPixelValue(p, i);
			maxValue.setGreater(p);
			minValue.setLower(p);
		}
	}

	private void calculateMaxRange() {
		int n = width * height;
		Neighborhood nbh = new Neighborhood(width, height);
		maxRange = newPixelValue();
		PixelValue_ p = newPixelValue();
		PixelValue_ pr = newPixelValue();
		PixelValue_ neighbor = newPixelValue();
		int q;
		for (int i = 0; i < n; i++) {
			getPixelValue(p, i);
			q = nbh.getNeighbor(i, 1);
			if (q != -1) {
				maxRange.setGreater(pr.range(p, getPixelValue(neighbor, q)));
			}
			q = nbh.getNeighbor(i, 3);
			if (q != -1) {
				maxRange.setGreater(pr.range(p, getPixelValue(neighbor, q)));
			}
		}
	}
};
